/*
 * 	<pre>
 *	EasyPeas.
 *  Copyright (C) 2007 S Owen
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2.1 of the License, or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * Jul 24, 2007
 * InvokerImpl.java
 * </pre>
 */
package org.easypeas.invocation;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.easypeas.EasyPeasConstants;
import org.easypeas.exceptions.ForwardNotFoundException;
import org.easypeas.exceptions.ResourceInvocationFailure;
import org.easypeas.exceptions.UnknownResourceException;
import org.easypeas.exceptions.UnresolvedMethodException;
import org.easypeas.mappings.Mappings;
import org.easypeas.mappings.Path;

/**
 * This the default implementation of an Invoker.
 *
 * From the mappings it determines the EasyPea mapped to the first element in
 * the path, and then uses the proceeding parts of the<br>
 * path to invoke accessors (or the special id(String) accessor if no defined
 * accessor matches), except the last element which is used to invoker either an
 * action of a view endpoint.<br>
 * <br>
 * e.g.<br>
 * <code>GET /queue/default/list</code><br>
 * will invoker the accessor mapped to <em>default</em> on the EasyPea mapped
 * to <em>queue</em>. The view method mapped to <em>list</em> is then
 * invoked on the EasyPea returned from that accessor. <br>
 * <code>GET /queue/items/27/name</code> may invoke something like
 * <code>Queue.getItems().id("27").showName(...)</code> <br>
 * If an action the forward is processed to determine the next path.<br>
 *
 *
 *
 * @see Invoker
 * @see View
 * @see Action
 * @see Accessor
 * @see EasyPea
 *
 * @author S Owen
 */
public class InvokerImpl implements Invoker {
 
    private static Logger logger = Logger.getLogger("org.easypeas.invocation.InvokerImpl");
    
    /**
     * {@inheritDoc}
     */
    public Map<String, Object> invokePath(HttpServletRequest request,
            HttpServletResponse response, Mappings mapping, String path)
            throws UnresolvedMethodException, UnknownResourceException,
            ResourceInvocationFailure {
        return executePath(mapping, request, path, response,false);
    }
    
    private Map<String, Object> executePath(final Mappings mapping, final HttpServletRequest request, final String path, final HttpServletResponse response, boolean isAction) throws ResourceInvocationFailure, UnknownResourceException, UnresolvedMethodException {
        
        Map<String,Object> result = null;
        Path pathObject = new Path(path);
        Object peaObject = mapping.getObject(pathObject.getRoot());
        if (peaObject == null) {
            throw new UnknownResourceException("Unable to find object for key:"+pathObject.getRoot());
        }
        
        for (List<Path.PathElement> elementList : pathObject.getPossibleElements()) {
            for (Path.PathElement element : elementList) {
                try {
                    if (element.type == Path.PathType.ENDPOINT) {
                        result = tryEndpoint(element, request, peaObject, mapping, response, isAction);
                        break;
                    } else if (element.type == Path.PathType.ACCESSOR) {
                        peaObject = tryAccessor(peaObject, element, mapping);
                        break;
                    } else if (element.type == Path.PathType.ID) {
                        peaObject = tryById(peaObject, element);
                        break;
                    }
                } catch(NoSuchMethodException e) {
                    if (element.type==Path.PathType.ID) {
                        //ID is the last resort, so if this method is not found then it must be an invalid path.
                        throw new UnresolvedMethodException("Unable to find suitable method for: "+element.name+" on " + peaObject.getClass());
                    }
                }
                
            }
            if (result!=null) {
                break;
            }
        }
        
        return result;
    }
    
    private Object tryById(Object peaObject, final Path.PathElement element) throws NoSuchMethodException, ResourceInvocationFailure, UnresolvedMethodException {
        Object newPeaObject = invokeById(peaObject,element.name);
        if (newPeaObject==null) {
            throw new UnresolvedMethodException("Object not found for id " + element.name + " on " + peaObject.getClass());
        }
        return newPeaObject;
    }
    
    private Object tryAccessor(Object peaObject, final Path.PathElement element, final Mappings mapping) throws NoSuchMethodException, ResourceInvocationFailure {
        Method accessorMethod = mapping.getAccessor(peaObject,element.name);
        return invokeAccessor(peaObject,accessorMethod);
    }
    
    private Map<String, Object> tryEndpoint(final Path.PathElement element, final HttpServletRequest request, final Object peaObject, final Mappings mapping, final HttpServletResponse response, final boolean isAction) throws NoSuchMethodException, ResourceInvocationFailure {
        Map<String, Object> result;
        Method method = null;
        if (!isAction) {
            method = mapping.getView(peaObject,element.name);
        } else {
            method = mapping.getAction(peaObject,element.name);
        }
        result = invokeEndpoint(request,response,mapping,peaObject,element.name,method);
        
        if (isAction) {
            result.put(EasyPeasConstants.EASYPEA_PEA,peaObject);
            result.put(EasyPeasConstants.EASYPEA_METHOD,element.name);
        }
        return result;
    }
    
    /**
     * {@inheritDoc}
     */
    public Map<String, Object> invokeActionPath(HttpServletRequest request,
            HttpServletResponse response, Mappings mapping, String path)
            throws UnresolvedMethodException, UnknownResourceException,
            ResourceInvocationFailure, ForwardNotFoundException {
        Map<String,Object> result = executePath(mapping, request, path, response,true);
        
        Object obj=result.get(EasyPeasConstants.EASYPEA_PEA);
        String actualPath = (String)result.get(EasyPeasConstants.EASYPEA_METHOD);
        
        result.remove(EasyPeasConstants.EASYPEA_PEA);
        result.remove(EasyPeasConstants.EASYPEA_METHOD);
        resolveForwardAddress(mapping, actualPath, result, obj);
        
        return result;
    }
    
    private void resolveForwardAddress(Mappings mappings, String path,
            Map<String, Object> result, Object obj)
            throws ForwardNotFoundException {
        String forward = (String) result.get(EasyPeasConstants.EASYPEA_FORWARD);
        if (forward == null) {
            throw new ForwardNotFoundException(
                    "Unable to find the forward key in the response map");
        }
        forward = mappings.getForward(obj, path, forward);
        if (forward == null) {
            throw new ForwardNotFoundException(
                    "Unable to find the forward key in the mappings");
        }
        result.put(EasyPeasConstants.EASYPEA_FORWARD, forward);
    }
    
    @SuppressWarnings("unchecked")
    private Map<String, Object> invokeEndpoint(HttpServletRequest request,
            HttpServletResponse response, Mappings mappings, Object obj,
            String methodKey, Method method) throws NoSuchMethodException,
            ResourceInvocationFailure {
        logger.finer("Trying method " + method + " with as endpoint "
                    + obj.getClass());        
        Map<String, Object> map = new HashMap<String, Object>();
        try {
            String forward = (String) method.invoke(obj, new Object[] {
                request, response, map });
            if (forward != null) {
                map.put(EasyPeasConstants.EASYPEA_FORWARD, forward);
            } else if (mappings.getViewTemplate(method) != null) {
                map.put(EasyPeasConstants.EASYPEA_TEMPLATE, mappings
                        .getViewTemplate(method));
            }
            
        } catch (IllegalArgumentException e) {
            throw new ResourceInvocationFailure(e);
        } catch (IllegalAccessException e) {
            throw new ResourceInvocationFailure(e);
        } catch (InvocationTargetException e) {
            throw new ResourceInvocationFailure(e.getTargetException());
        }
        map.put(EasyPeasConstants.EASYPEA_PEA, obj);
        map.put(EasyPeasConstants.EASYPEA_METHOD, methodKey);
        return map;
    }
    
    private Object invokeById(Object obj, String id)
    throws ResourceInvocationFailure, NoSuchMethodException {
        logger
                .finer("Trying to find by id " + id + " on "
                    + obj.getClass());
        Method m;
        try {
            m = obj.getClass().getMethod("id", new Class[] { String.class });
            return m.invoke(obj, new Object[] { id });
        } catch (SecurityException e) {
            throw new ResourceInvocationFailure(e);
        } catch (IllegalArgumentException e) {
            throw new ResourceInvocationFailure(e);
        } catch (IllegalAccessException e) {
            throw new ResourceInvocationFailure(e);
        } catch (InvocationTargetException e) {
            throw new ResourceInvocationFailure(e);
        }
    }
    
    private Object invokeAccessor(Object obj, Method method)
    throws NoSuchMethodException, ResourceInvocationFailure {
        logger.finer("Trying method " + method + " with no paramers on "
                    + obj.getClass());
        try {
            return method.invoke(obj, new Object[] {});
        } catch (IllegalAccessException e) {
            throw new ResourceInvocationFailure(
                    "Illegal Access whilst invoking " + method + " on "
                    + obj.getClass(), e);
        } catch (InvocationTargetException e) {
            throw new ResourceInvocationFailure("Failure whilst invoking "
                    + method + " on " + obj.getClass(), e);
        }
    }
}
